Micron Document
C.S.Burner 🪙 ━►🔥GIT Node


Displaying Raw • Download

meshchatx/src/frontend/components/settings/IdentitiesPage.vue 9ec2a88817819d433c5d68dbec0ca070ca989b91 (9ec2a888) Text, 32.98 KB

T8b949e<!-- SPDX-License-Identifier: 0BSD -->

Tff7b72<templateTff7b72>
Tff7b72<div
Te6edf3class=Ta5d6ff"Ta5d6ffflex flex-col flex-1 overflow-hidden min-w-0 bg-gradient-to-br from-slate-50 via-slate-100 to-white dark:from-zinc-950 dark:via-zinc-900 dark:to-zinc-900Ta5d6ff"
Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex-1 overflow-y-auto w-full px-4 md:px-5 lg:px-8 py-6Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffspace-y-6 w-full max-w-4xl mx-autoTa5d6ff"Tff7b72>
T8b949e<!-- header -->
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex flex-col gap-4 sm:flex-row sm:items-start sm:justify-betweenTa5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffmin-w-0Ta5d6ff"Tff7b72>
Tff7b72<h1 Te6edf3class=Ta5d6ff"Ta5d6fftext-3xl font-bold text-gray-900 dark:text-white tracking-tightTa5d6ff"Tff7b72>
{{ Te6edf3$t("identities.title") }}
Tff7b72</h1>
Tff7b72<p Te6edf3class=Ta5d6ff"Ta5d6fftext-gray-600 dark:text-gray-400 mt-1Ta5d6ff"Tff7b72>
{{ Te6edf3$t("identities.manage") }}
Tff7b72</p>
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex flex-row gap-2 sm:flex-wrap sm:items-stretch sm:justify-endTa5d6ff"Tff7b72>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffinline-flex items-center justify-center gap-x-2 rounded-xl bg-blue-600 p-2.5 sm:px-4 sm:py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 transition-all active:scale-[0.98] sm:rounded-2xlTa5d6ff"
Te6edf3:title=Ta5d6ff"Te6edf3$tTa5d6ff(Ta5d6ff'Ta5d6ffidentities.new_identityTa5d6ff'Ta5d6ff)Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffshowCreateModal = trueTa5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffplusTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffw-5 h-5 shrink-0Ta5d6ff" Tff7b72/>
Tff7b72<span Te6edf3class=Ta5d6ff"Ta5d6ffhidden sm:inline truncateTa5d6ff"Tff7b72>{{ Te6edf3$t("identities.new_identity") }}Tff7b72</span>
Tff7b72</button>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffinline-flex items-center justify-center gap-x-2 rounded-xl border border-gray-300 dark:border-zinc-600 bg-white dark:bg-zinc-800 p-2.5 sm:px-4 sm:py-2.5 text-sm font-semibold text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-zinc-700 transition-all active:scale-[0.98] sm:rounded-2xlTa5d6ff"
Te6edf3:title=Ta5d6ff"Te6edf3$tTa5d6ff(Ta5d6ff'Ta5d6ffidentities.importTa5d6ff'Ta5d6ff)Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffshowImportModal = trueTa5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffuploadTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffw-5 h-5 shrink-0Ta5d6ff" Tff7b72/>
Tff7b72<span Te6edf3class=Ta5d6ff"Ta5d6ffhidden sm:inline truncateTa5d6ff"Tff7b72>{{ Te6edf3$t("identities.import") }}Tff7b72</span>
Tff7b72</button>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffinline-flex items-center justify-center gap-x-2 rounded-xl border border-gray-300 dark:border-zinc-600 bg-white dark:bg-zinc-800 p-2.5 sm:px-4 sm:py-2.5 text-sm font-semibold text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-zinc-700 transition-all active:scale-[0.98] disabled:opacity-50 sm:rounded-2xlTa5d6ff"
Te6edf3:disabled=Ta5d6ff"Ta5d6ffidentities.length === 0Ta5d6ff"
Te6edf3:title=Ta5d6ff"Te6edf3$tTa5d6ff(Ta5d6ff'Ta5d6ffidentities.export_allTa5d6ff'Ta5d6ff)Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffdownloadAllIdentitiesTa5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6fffile-exportTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffw-5 h-5 shrink-0Ta5d6ff" Tff7b72/>
Tff7b72<span Te6edf3class=Ta5d6ff"Ta5d6ffhidden sm:inline truncateTa5d6ff"Tff7b72>{{ Te6edf3$t("identities.export_all") }}Tff7b72</span>
Tff7b72</button>
Tff7b72</div>
Tff7b72</div>
Tff7b72<input
Te6edf3ref=Ta5d6ff"Ta5d6ffidentityFileInputTa5d6ff"
Te6edf3type=Ta5d6ff"Ta5d6fffileTa5d6ff"
Te6edf3accept=Ta5d6ff"Ta5d6ff.identity,.bin,.keyTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffhiddenTa5d6ff"
@Te6edf3change=Ta5d6ff"Ta5d6ffonIdentityRestoreFileChangeTa5d6ff"
Tff7b72/>

T8b949e<!-- identities list -->
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffgrid gap-4Ta5d6ff"Tff7b72>
Tff7b72<template Te6edf3v-if=Ta5d6ff"Ta5d6ffisLoading && identities.length === 0Ta5d6ff"Tff7b72>
Tff7b72<div
Te6edf3v-for=Ta5d6ff"Ta5d6ffi in 4Ta5d6ff"
Te6edf3:key=Ta5d6ff"Ta5d6ff'Ta5d6ffskel-Ta5d6ff'Ta5d6ff + iTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffglass-card overflow-hidden p-5 flex items-center gap-4Ta5d6ff"
Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffw-14 h-14 rounded-2xl bg-gray-200 dark:bg-zinc-700 animate-pulse shrink-0Ta5d6ff" Tff7b72/>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex-1 min-w-0 space-y-2Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffh-5 w-32 bg-gray-200 dark:bg-zinc-700 rounded animate-pulseTa5d6ff" Tff7b72/>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffh-3 w-48 bg-gray-100 dark:bg-zinc-800 rounded animate-pulseTa5d6ff" Tff7b72/>
Tff7b72</div>
Tff7b72</div>
Tff7b72</template>
Tff7b72<div
Te6edf3v-for=Ta5d6ff"Ta5d6ffidentity in identitiesTa5d6ff"
v-else
Te6edf3:key=Ta5d6ff"Ta5d6ffidentity.hashTa5d6ff"
Te6edf3v-memo=Ta5d6ff"Ta5d6ff[
identity.hash,
identity.is_current,
identity.display_name,
identity.lxmf_address,
identity.lxst_address,
identity.message_count,
identity.icon_name,
identity.icon_background_colour,
identity.icon_foreground_colour,
]Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffglass-card overflow-hidden group transition-all duration-300Ta5d6ff"
Te6edf3:class=Ta5d6ff"Ta5d6ff{
Ta5d6ff'Ta5d6ffring-2 ring-blue-500/50 dark:ring-blue-400/40 bg-blue-50/30 dark:bg-blue-900/10Ta5d6ff'Ta5d6ff:
identity.is_current,
}Ta5d6ff"
Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffp-4 sm:p-5 flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-4Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex items-start gap-3 sm:gap-4 sm:flex-1 sm:min-w-0Ta5d6ff"Tff7b72>
T8b949e<!-- icon -->
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffrelative shrink-0Ta5d6ff"Tff7b72>
Tff7b72<div
Te6edf3class=Ta5d6ff"Ta5d6ffw-12 h-12 sm:w-14 sm:h-14 rounded-2xl flex items-center justify-center shadow-inner overflow-hidden transition-all duration-500Ta5d6ff"
Te6edf3:class=Ta5d6ff"
Ta5d6ff identity.is_current && !identity.icon_background_colour
? Ta5d6ff'Ta5d6ffbg-gradient-to-br from-blue-100 to-indigo-100 dark:from-blue-900/50 dark:to-indigo-900/50Ta5d6ff'
Ta5d6ff : !identity.icon_background_colour
? Ta5d6ff'Ta5d6ffbg-gradient-to-br from-gray-100 to-slate-100 dark:from-zinc-800 dark:to-zinc-800/50Ta5d6ff'
Ta5d6ff : Ta5d6ff'Ta5d6ff'
Ta5d6ff Ta5d6ff"
Te6edf3:style=Ta5d6ff"
Ta5d6ff identity.icon_background_colour
? { Ta5d6ff'Ta5d6ffbackground-colorTa5d6ff'Ta5d6ff: identity.icon_background_colour }
: {}
Ta5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon
Te6edf3v-if=Ta5d6ff"Ta5d6ffidentity.icon_nameTa5d6ff"
Te6edf3:icon-name=Ta5d6ff"Ta5d6ffidentity.icon_nameTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffw-7 h-7 sm:w-8 sm:h-8Ta5d6ff"
Te6edf3:style=Ta5d6ff"Ta5d6ff{ color: identity.icon_foreground_colour || Ta5d6ff'Ta5d6ffinheritTa5d6ff'Ta5d6ff }Ta5d6ff"
Tff7b72/>
Tff7b72<MaterialDesignIcon
v-else
Te6edf3:icon-name=Ta5d6ff"Ta5d6ffidentity.is_current ? Ta5d6ff'Ta5d6ffaccount-checkTa5d6ff'Ta5d6ff : Ta5d6ff'Ta5d6ffaccountTa5d6ff'Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffw-7 h-7 sm:w-8 sm:h-8Ta5d6ff"
Te6edf3:class=Ta5d6ff"
Ta5d6ff identity.is_current
? Ta5d6ff'Ta5d6fftext-blue-600 dark:text-blue-400Ta5d6ff'
Ta5d6ff : Ta5d6ff'Ta5d6fftext-gray-500 dark:text-gray-400Ta5d6ff'
Ta5d6ff Ta5d6ff"
Tff7b72/>
Tff7b72</div>
Tff7b72<div
Te6edf3v-if=Ta5d6ff"Ta5d6ffidentity.is_currentTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffabsolute -top-1 -right-1 w-4 h-4 bg-emerald-500 rounded-full border-2 border-white dark:border-zinc-900 shadow-smTa5d6ff"
Tff7b72>Tff7b72</div>
Tff7b72</div>

T8b949e<!-- info -->
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex-1 min-w-0Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex flex-wrap items-center gap-2Ta5d6ff"Tff7b72>
Tff7b72<h3 Te6edf3class=Ta5d6ff"Ta5d6fffont-bold text-gray-900 dark:text-white break-words sm:truncateTa5d6ff"Tff7b72>
{{ identity.display_name }}
Tff7b72</h3>
Tff7b72<span
Te6edf3v-if=Ta5d6ff"Ta5d6ffidentity.is_currentTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffpx-2 py-0.5 rounded-full bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300 text-[10px] font-bold uppercase tracking-widerTa5d6ff"
Tff7b72>
{{ Te6edf3$t("identities.current") }}
Tff7b72</span>
Tff7b72</div>
Tff7b72<div
Te6edf3class=Ta5d6ff"Ta5d6fftext-xs font-mono text-gray-500 dark:text-zinc-500 mt-0.5 tracking-tight break-all sm:truncateTa5d6ff"
Te6edf3:title=Ta5d6ff"Ta5d6ff'Ta5d6ffRNS: Ta5d6ff'Ta5d6ff + identity.hashTa5d6ff"
Tff7b72>
ID: {{ identity.hash }}
Tff7b72</div>
Tff7b72<div
Te6edf3v-if=Ta5d6ff"Ta5d6ffidentity.lxmf_addressTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6fftext-[10px] font-mono text-gray-400 dark:text-zinc-600 mt-0.5 tracking-tighter break-all sm:truncateTa5d6ff"
Te6edf3:title=Ta5d6ff"Ta5d6ff'Ta5d6ffLXMF: Ta5d6ff'Ta5d6ff + identity.lxmf_addressTa5d6ff"
Tff7b72>
LXMF: {{ identity.lxmf_address }}
Tff7b72</div>
Tff7b72<div
Te6edf3v-if=Ta5d6ff"Ta5d6ffidentity.lxst_addressTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6fftext-[10px] font-mono text-gray-400 dark:text-zinc-600 mt-0.5 tracking-tighter break-all sm:truncateTa5d6ff"
Te6edf3:title=Ta5d6ff"Ta5d6ff'Ta5d6ffLXST: Ta5d6ff'Ta5d6ff + identity.lxst_addressTa5d6ff"
Tff7b72>
LXST: {{ identity.lxst_address }}
Tff7b72</div>
Tff7b72<div
Te6edf3v-if=Ta5d6ff"Ta5d6ffidentity.message_count != nullTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6fftext-[10px] text-gray-400 dark:text-zinc-500 mt-0.5Ta5d6ff"
Tff7b72>
{{ Te6edf3$t("identities.message_count", { count: identity.message_count }) }}
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>

T8b949e<!-- actions -->
Tff7b72<div
Te6edf3class=Ta5d6ff"Ta5d6ffflex items-center justify-end gap-2 border-t border-gray-100 dark:border-zinc-800 pt-3 sm:border-0 sm:pt-0 sm:shrink-0Ta5d6ff"
Tff7b72>
Tff7b72<template Te6edf3v-if=Ta5d6ff"Ta5d6ffidentity.is_currentTa5d6ff"Tff7b72>
Tff7b72<div
Te6edf3class=Ta5d6ff"Ta5d6ffflex items-center gap-1.5 sm:opacity-0 sm:group-hover:opacity-100 transition-opacityTa5d6ff"
Tff7b72>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffp-2 sm:p-2.5 rounded-xl bg-gray-100 dark:bg-zinc-800 text-gray-700 dark:text-gray-300 hover:bg-amber-500 hover:text-white dark:hover:bg-amber-600 transition-all active:scale-90Ta5d6ff"
Te6edf3:title=Ta5d6ff"Te6edf3$tTa5d6ff(Ta5d6ff'Ta5d6ffidentities.export_key_fileTa5d6ff'Ta5d6ff)Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffdownloadIdentityFileTa5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6fffile-exportTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffw-5 h-5Ta5d6ff" Tff7b72/>
Tff7b72</button>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffp-2 sm:p-2.5 rounded-xl bg-gray-100 dark:bg-zinc-800 text-gray-700 dark:text-gray-300 hover:bg-amber-500 hover:text-white dark:hover:bg-amber-600 transition-all active:scale-90Ta5d6ff"
Te6edf3:title=Ta5d6ff"Te6edf3$tTa5d6ff(Ta5d6ff'Ta5d6ffidentities.copy_base32Ta5d6ff'Ta5d6ff)Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffcopyIdentityBase32Ta5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffcontent-copyTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffw-5 h-5Ta5d6ff" Tff7b72/>
Tff7b72</button>
Tff7b72</div>
Tff7b72</template>
Tff7b72<button
Te6edf3v-if=Ta5d6ff"Ta5d6ff!identity.is_currentTa5d6ff"
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffp-2 sm:p-2.5 rounded-xl bg-gray-100 dark:bg-zinc-800 text-gray-700 dark:text-gray-300 hover:bg-blue-500 hover:text-white dark:hover:bg-blue-600 transition-all active:scale-90Ta5d6ff"
Te6edf3:title=Ta5d6ff"Te6edf3$tTa5d6ff(Ta5d6ff'Ta5d6ffidentities.switchTa5d6ff'Ta5d6ff)Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffswitchIdentity(identity)Ta5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffswap-horizontalTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffw-5 h-5Ta5d6ff" Tff7b72/>
Tff7b72</button>
Tff7b72<button
Te6edf3v-if=Ta5d6ff"Ta5d6ff!identity.is_currentTa5d6ff"
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffp-2 sm:p-2.5 rounded-xl bg-gray-100 dark:bg-zinc-800 text-gray-700 dark:text-gray-300 hover:bg-red-500 hover:text-white dark:hover:bg-red-600 transition-all active:scale-90Ta5d6ff"
Te6edf3:title=Ta5d6ff"Te6edf3$tTa5d6ff(Ta5d6ff'Ta5d6ffidentities.deleteTa5d6ff'Ta5d6ff)Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffdeleteIdentity(identity)Ta5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffdelete-outlineTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffw-5 h-5Ta5d6ff" Tff7b72/>
Tff7b72</button>
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>

T8b949e<!-- empty state -->
Tff7b72<div Te6edf3v-if=Ta5d6ff"Ta5d6ff!isLoading && identities.length === 0Ta5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffglass-card p-12 text-centerTa5d6ff"Tff7b72>
Tff7b72<div
Te6edf3class=Ta5d6ff"Ta5d6ffw-20 h-20 bg-gray-100 dark:bg-zinc-800 rounded-3xl flex items-center justify-center mx-auto mb-4Ta5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffaccount-groupTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffw-10 h-10 text-gray-400Ta5d6ff" Tff7b72/>
Tff7b72</div>
Tff7b72<h3 Te6edf3class=Ta5d6ff"Ta5d6fftext-xl font-bold text-gray-900 dark:text-whiteTa5d6ff"Tff7b72>
{{ Te6edf3$t("identities.no_identities") }}
Tff7b72</h3>
Tff7b72<p Te6edf3class=Ta5d6ff"Ta5d6fftext-gray-500 dark:text-gray-400 mt-2Ta5d6ff"Tff7b72>{{ Te6edf3$t("identities.create_first") }}Tff7b72</p>
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>

T8b949e<!-- create modal -->
Tff7b72<div
Te6edf3v-if=Ta5d6ff"Ta5d6ffshowCreateModalTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6fffixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/40 backdrop-blur-smTa5d6ff"
Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffglass-card w-full max-w-md shadow-2xl animate-in fade-in zoom-in duration-200Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffp-6Ta5d6ff"Tff7b72>
Tff7b72<h2 Te6edf3class=Ta5d6ff"Ta5d6fftext-2xl font-bold text-gray-900 dark:text-whiteTa5d6ff"Tff7b72>
{{ Te6edf3$t("identities.new_identity") }}
Tff7b72</h2>
Tff7b72<p Te6edf3class=Ta5d6ff"Ta5d6fftext-gray-500 dark:text-gray-400 mt-1Ta5d6ff"Tff7b72>{{ Te6edf3$t("identities.generate_fresh") }}Tff7b72</p>

Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffmt-6 space-y-4Ta5d6ff"Tff7b72>
Tff7b72<divTff7b72>
Tff7b72<label Te6edf3class=Ta5d6ff"Ta5d6ffblock text-sm font-semibold text-gray-700 dark:text-gray-300 mb-1.5Ta5d6ff"Tff7b72>
{{ Te6edf3$t("identities.display_name") }}
Tff7b72</label>
Tff7b72<input
Te6edf3v-model=Ta5d6ff"Ta5d6ffnewIdentityNameTa5d6ff"
Te6edf3type=Ta5d6ff"Ta5d6fftextTa5d6ff"
Te6edf3:placeholder=Ta5d6ff"Te6edf3$tTa5d6ff(Ta5d6ff'Ta5d6ffidentities.display_name_hintTa5d6ff'Ta5d6ff)Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffinput-fieldTa5d6ff"
autofocus
@keyup.Te6edf3enter=Ta5d6ff"Ta5d6ffcreateIdentityTa5d6ff"
Tff7b72/>
Tff7b72</div>
Tff7b72</div>

Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffmt-8 flex gap-3Ta5d6ff"Tff7b72>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffflex-1 px-4 py-2.5 rounded-xl border border-gray-200 dark:border-zinc-700 text-sm font-semibold text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-zinc-800 transitionTa5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffshowCreateModal = falseTa5d6ff"
Tff7b72>
{{ Te6edf3$t("common.cancel") }}
Tff7b72</button>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffflex-1 px-4 py-2.5 rounded-xl bg-blue-600 text-white text-sm font-semibold shadow-lg shadow-blue-500/25 hover:bg-blue-500 transition active:scale-95Ta5d6ff"
Te6edf3:disabled=Ta5d6ff"Ta5d6ffisCreatingTa5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffcreateIdentityTa5d6ff"
Tff7b72>
Tff7b72<span Te6edf3v-if=Ta5d6ff"Ta5d6ffisCreatingTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffanimate-spin mr-2Ta5d6ff"Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffloadingTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffw-4 h-4Ta5d6ff" Tff7b72/>
Tff7b72</span>
{{ Te6edf3$t("common.add") }}
Tff7b72</button>
Tff7b72</div>
Tff7b72</div>

T8b949e<!-- import modal -->
Tff7b72<div
Te6edf3v-if=Ta5d6ff"Ta5d6ffshowImportModalTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6fffixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/40 backdrop-blur-smTa5d6ff"
@click.Te6edf3self=Ta5d6ff"Ta5d6ffshowImportModal = falseTa5d6ff"
Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffglass-card w-full max-w-md shadow-2xl animate-in fade-in zoom-in duration-200Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffp-6Ta5d6ff"Tff7b72>
Tff7b72<h2 Te6edf3class=Ta5d6ff"Ta5d6fftext-2xl font-bold text-gray-900 dark:text-whiteTa5d6ff"Tff7b72>
{{ Te6edf3$t("identities.import") }}
Tff7b72</h2>
Tff7b72<p Te6edf3class=Ta5d6ff"Ta5d6fftext-gray-500 dark:text-gray-400 mt-1Ta5d6ff"Tff7b72>{{ Te6edf3$t("identities.import_hint") }}Tff7b72</p>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffmt-6 space-y-4Ta5d6ff"Tff7b72>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffw-full secondary-chip justify-centerTa5d6ff"
@Te6edf3click=Ta5d6ff"
Ta5d6ff Te6edf3$refs.identityFileInputTa5d6ff?.click();
showImportModal = false;
Ta5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffuploadTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffw-4 h-4Ta5d6ff" Tff7b72/>
{{ Te6edf3$t("identities.upload_key_file") }}
Tff7b72</button>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffborder-t border-gray-200 dark:border-zinc-700 pt-4Ta5d6ff"Tff7b72>
Tff7b72<label Te6edf3class=Ta5d6ff"Ta5d6ffblock text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2Ta5d6ff"Tff7b72>
{{ Te6edf3$t("identities.paste_base32") }}
Tff7b72</label>
Tff7b72<textarea
Te6edf3v-model=Ta5d6ff"Ta5d6ffidentityRestoreBase32Ta5d6ff"
Te6edf3rows=Ta5d6ff"Ta5d6ff3Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffinput-field font-mono text-xs w-fullTa5d6ff"
Te6edf3:placeholder=Ta5d6ff"Te6edf3$tTa5d6ff(Ta5d6ff'Ta5d6ffidentities.paste_base32_placeholderTa5d6ff'Ta5d6ff)Ta5d6ff"
Tff7b72/>
Tff7b72<div
Te6edf3v-if=Ta5d6ff"Ta5d6ffidentityRestoreErrorTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6fftext-sm text-red-600 dark:text-red-400 mt-2Ta5d6ff"
Tff7b72>
{{ identityRestoreError }}
Tff7b72</div>
Tff7b72<div
Te6edf3v-if=Ta5d6ff"Ta5d6ffidentityRestoreMessageTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6fftext-sm text-green-600 dark:text-green-400 mt-2Ta5d6ff"
Tff7b72>
{{ identityRestoreMessage }}
Tff7b72</div>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffprimary-chip mt-3Ta5d6ff"
Te6edf3:disabled=Ta5d6ff"Ta5d6ffidentityRestoreInProgress || !identityRestoreBase32.trim()Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffrestoreIdentityBase32Ta5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon
Te6edf3v-if=Ta5d6ff"Ta5d6ffidentityRestoreInProgressTa5d6ff"
Te6edf3icon-name=Ta5d6ff"Ta5d6ffloadingTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffw-4 h-4 animate-spinTa5d6ff"
Tff7b72/>
{{
identityRestoreInProgress
? Te6edf3$t("identities.restoring")
: Te6edf3$t("identities.confirm_restore")
}}
Tff7b72</button>
Tff7b72</div>
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffmt-6Ta5d6ff"Tff7b72>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffw-full px-4 py-2.5 rounded-xl border border-gray-200 dark:border-zinc-700 text-sm font-semibold text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-zinc-800Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffshowImportModal = falseTa5d6ff"
Tff7b72>
{{ Te6edf3$t("common.cancel") }}
Tff7b72</button>
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>
Tff7b72</template>

Tff7b72<scriptTff7b72>
import MaterialDesignIcon from "../MaterialDesignIcon.vue";
import ToastUtils from "../../js/ToastUtils";
import DialogUtils from "../../js/DialogUtils";
import GlobalEmitter from "../../js/GlobalEmitter";

export default {
name: "IdentitiesPage",
components: {
MaterialDesignIcon,
},
data() {
return {
identities: [],
isLoading: false,
showCreateModal: false,
showImportModal: false,
newIdentityName: "",
isCreating: false,
identityRestoreBase32: "",
identityRestoreInProgress: false,
identityRestoreMessage: "",
identityRestoreError: "",
identityRestoreFile: null,
};
},
computed: {
currentIdentity() {
return this.identities.find((i) => i.is_current) || null;
},
},
mounted() {
this.getIdentities();
GlobalEmitter.on("identity-switched", this.onIdentitySwitched);
},
beforeUnmount() {
GlobalEmitter.off("identity-switched", this.onIdentitySwitched);
},
methods: {
onIdentitySwitched() {
this.getIdentities();
this.isCreating = false;
},
async getIdentities() {
this.isLoading = true;
try {
const response = await window.api.get("/api/v1/identities");
this.identities = response.data?.identities ?? [];
} catch (e) {
console.error(e);
ToastUtils.error(this.$t("identities.failed_load"));
} finally {
this.isLoading = false;
}
},
async downloadIdentityFile() {
try {
const response = await window.api.get("/api/v1/identity/backup/download", {
responseType: "blob",
});
const blob = new Blob([response.data], { type: "application/octet-stream" });
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", "identity");
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
ToastUtils.success(this.$t("identities.identity_exported"));
} catch {
ToastUtils.error(this.$t("identities.identity_export_failed"));
}
},
async copyIdentityBase32() {
try {
const response = await window.api.get("/api/v1/identity/backup/base32");
const base32 = response.data?.identity_base32 ?? "";
if (!base32) {
ToastUtils.error(this.$t("identities.no_identity_available"));
return;
}
await navigator.clipboard.writeText(base32);
ToastUtils.success(this.$t("identities.identity_copied"));
} catch {
ToastUtils.error(this.$t("identities.identity_copy_failed"));
}
},
async downloadAllIdentities() {
try {
const response = await window.api.get("/api/v1/identities/export-all", {
responseType: "blob",
});
const url = window.URL.createObjectURL(new Blob([response.data], { type: "application/zip" }));
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", "identities_export.zip");
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(url);
ToastUtils.success(this.$t("identities.export_all_success"));
} catch (e) {
const msg = e?.response?.data?.message || this.$t("identities.export_all_failed");
ToastUtils.error(msg);
}
},
onIdentityRestoreFileChange(event) {
const files = event.target.files;
if (files?.[0]) {
this.identityRestoreFile = files[0];
this.identityRestoreError = "";
this.identityRestoreMessage = "";
this.restoreIdentityFile();
}
event.target.value = "";
},
async restoreIdentityFile() {
if (this.identityRestoreInProgress || !this.identityRestoreFile) return;
this.identityRestoreInProgress = true;
this.identityRestoreMessage = "";
this.identityRestoreError = "";
try {
const formData = new FormData();
formData.append("file", this.identityRestoreFile);
const response = await window.api.post("/api/v1/identity/restore", formData, {
headers: { "Content-Type": "multipart/form-data" },
});
this.identityRestoreMessage = response.data?.message ?? this.$t("identities.identity_restored");
this.identityRestoreFile = null;
this.showImportModal = false;
} catch {
this.identityRestoreError = this.$t("identities.identity_restore_failed");
} finally {
this.identityRestoreInProgress = false;
}
},
async restoreIdentityBase32() {
if (this.identityRestoreInProgress || !this.identityRestoreBase32?.trim()) return;
this.identityRestoreInProgress = true;
this.identityRestoreMessage = "";
this.identityRestoreError = "";
try {
const response = await window.api.post("/api/v1/identity/restore", {
base32: this.identityRestoreBase32.trim(),
});
this.identityRestoreMessage = response.data?.message ?? this.$t("identities.identity_restored");
this.identityRestoreBase32 = "";
this.showImportModal = false;
} catch {
this.identityRestoreError = this.$t("identities.identity_restore_failed");
} finally {
this.identityRestoreInProgress = false;
}
},
async createIdentity() {
if (!this.newIdentityName) {
ToastUtils.warning(this.$t("identities.enter_display_name_warning"));
return;
}

this.isCreating = true;
try {
await window.api.post("/api/v1/identities/create", {
display_name: this.newIdentityName,
});
ToastUtils.success(this.$t("identities.created"));
this.showCreateModal = false;
this.newIdentityName = "";
await this.getIdentities();
} catch (e) {
console.error(e);
ToastUtils.error(this.$t("identities.failed_create"));
} finally {
this.isCreating = false;
}
},
async switchIdentity(identity) {
if (identity.is_current) return;

if (!(await DialogUtils.confirm(this.$t("identities.switch_confirm", { name: identity.display_name })))) {
return;
}

try {
this.isCreating = true;
GlobalEmitter.emit("identity-switching-start");

const response = await window.api.post("/api/v1/identities/switch", {
identity_hash: identity.hash,
});

if (response.data.hotswapped) {
// Success is handled by GlobalEmitter "identity-switched" which we listen to
ToastUtils.success(this.$t("identities.switched") || "Identity switched successfully");
} else {
ToastUtils.info(this.$t("identities.switch_scheduled"));
setTimeout(() => {
window.location.reload();
}, 2000);
}
} catch (e) {
console.error(e);
const errorMsg =
e.response?.data?.message || this.$t("identities.failed_switch") || "Failed to switch identity";
ToastUtils.error(errorMsg);
this.isCreating = false;

// If it was a partial failure, we might need to reload anyway to be safe,
// but let's try to stay on the page if hotswap just failed.
GlobalEmitter.emit("identity-switched"); // To clear any global loading overlays
}
},
async deleteIdentity(identity) {
if (!(await DialogUtils.confirm(this.$t("identities.delete_confirm", { name: identity.display_name })))) {
return;
}

try {
await window.api.delete(`/api/v1/identities/${identity.hash}`);
ToastUtils.success(this.$t("identities.deleted"));
await this.getIdentities();
} catch (e) {
console.error(e);
ToastUtils.error(this.$t("identities.failed_delete"));
}
},
},
};
Tff7b72</script>

Tff7b72<style scopedTff7b72>
.glass-card {
@apply bg-white/90 dark:bg-zinc-900/80 backdrop-blur border border-gray-200 dark:border-zinc-800 rounded-3xl shadow-lg;
}
.input-field {
@apply bg-gray-50/90 dark:bg-zinc-800/80 border border-gray-200 dark:border-zinc-700 text-sm rounded-xl focus:ring-2 focus:ring-blue-400 focus:border-blue-400 dark:focus:ring-blue-500 dark:focus:border-blue-500 block w-full p-3 text-gray-900 dark:text-gray-100 transition;
}
Tff7b72</style>

Served by rngit 1.5.2 - Generated in 0.05s